"""
水平拆分分为两种:
    水平分表
    水平分库

水平分表:
    将 一张表的记录 拆分到多张表中
    对于记录较多的表, 会出现索引膨胀, 查询超时等问题, 影响用户体验
水平分库:
    水平分表后, 将分表分散放在多个数据库节点中

拆分规则:
    时间
        按照时间切分，就是将6个月前，甚至一年前的数据切出去放到另外的一张表，因为随着时间流逝，这些表的数据
        被查询的概率变小，所以没必要和“热数据”放在一起，这个也是“冷热数据分离”。
    业务
        按照业务将数据进行分类并拆分, 如文章包含金融、科技等多个分类, 可以每个分类的数据拆分到一张表中。
    ID范围
        从 0 到 100W 一个表，100W+1 到 200W 一个表。
    HASH取模 离散化
        取用户id，然后hash取模，分配到不同的数据库上。这样可以同时向多个表中插入数据, 提高并发能力, 同时由于用户id进行了离散处理, 不会出现ID冲突的问题
    地理区域
        比如按照华东，华南，华北这样来区分业务，部分云服务应该就是如此

数据库定向查询:
    如果进行了水平拆分, 在没有精确过滤条件的情况下, 可能需要到多个数据库中依次查询目标数据
    可以对 RoutingSession 进行二次开发, 提供方法进行 数据库定向查询

    示例场景如下:对用户表进行水平分库分表, 用户数据分别保存在 db1.t_user 和 db2.t_user 中, 项目的其他数据保存在数据库 test 中
"""
import random
from sqlalchemy import orm, create_engine
from flask_sqlalchemy import SQLAlchemy, SignallingSession, get_state
from flask import Flask
from sqlalchemy.orm import Session as SessionBase
from sqlalchemy.sql.dml import UpdateBase


# 1.实现自定义的 session类, 继承 SignallingSession类, 重写 get_bind方法, 根据读写需求选择对应的数据库地址
class RoutingSession(SignallingSession):
    def __init__(self, db, autocommit=False, autoflush=True, **options):
        super(RoutingSession, self).__init__(db, autocommit, autoflush, **options)
        # 每个session(每次请求)只做一次从库的随机处理
        self.slave_key = random.choice(['slave'])

    def get_bind(self, mapper=None, clause=None):
        """每次数据库操作(增删改查及事务操作)都会调用该方法, 来获取对应的数据库引擎(访问的数据库)"""

        state = get_state(self.app)
        # 如果语句指定了要访问的数据库, 则直接返回对应的地址
        if self._bind:
            return state.db.get_engine(self.app, bind=self._bind)

        # 如果模型类指定了具体要访问的数据库, 则直接访问对应的数据库
        elif mapper is not None:
            try:
                # SA >= 1.3
                persist_selectable = mapper.persist_selectable
            except AttributeError:
                # SA < 1.3
                persist_selectable = mapper.mapped_table

            info = getattr(persist_selectable, 'info', {})
            bind_key = info.get('bind_key')
            if bind_key is not None:
                return state.db.get_engine(self.app, bind=bind_key)

        # 如果模型类没有指定要访问的库, 则访问默认的数据库(SQLALCHEMY_DATABASE_URI指定的库)
        # return SessionBase.get_bind(self, mapper, clause)

        # 如果模型类没有指定要访问的库, 则根据读写行为访问对应的主从数据库
        # isinstance(对象, 类)  判断该对象是否为指定的类型  True/False
        if self._flushing or isinstance(clause, UpdateBase):  # 增删改操作
            print('写操作')
            # return engines['master']
            return state.db.get_engine(self.app, bind='master')

        else:  # 查询操作

            print('读操作: %s' % self.slave_key)
            return state.db.get_engine(self.app, bind=self.slave_key)

    _bind = None  # 定义类属性,记录要访问的数据库,避免报错

    def using_bind(self, bind):
        """
        指定语句对应访问的数据库
        :param bind: 数据库别名
        :return: session对象
        """
        self._bind = bind
        return self


# 2.实现自定义的 SQLAlchemy类, 继承 SQLAlchemy类, 重写 create_session方法, 在内部使用自定义的 Session类
class RoutingSQLAlchemny(SQLAlchemy):
    def create_session(self, options):
        return orm.sessionmaker(class_=RoutingSession, db=self, **options)


app = Flask(__name__)
# 设置单个数据库URI (用于建表并添加测试数据)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@192.168.19.137:3306/test'

# 设置多个数据库的URI (用于数据操作)
app.config['SQLALCHEMY_BINDS'] = {
    'db1': 'mysql://root:mysql@192.168.19.137:3306/db1',
    'db2': 'mysql://root:mysql@192.168.19.137:3306/db2',
    'master': 'mysql://root:mysql@192.168.19.137:3306/test',
    'slave': 'mysql://root:mysql@192.168.19.137:3306/test',
}

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False

db = RoutingSQLAlchemny(app)


# print(db.session())  # 这样使用的session就变为 RoutingSession
# # db.session 不加括号 session使用默认的 scoped_session 对象
# print(db.session)


# 构建模型类
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    age = db.Column(db.Integer, default=0)


@app.route('/')
def index():
    """需求: 用户数据存放到了db1和db2中, 需要遍历多个数据库查询zs的数据"""
    for db_name in ['db1', 'db2']:
        data = db.session().using_bind(db_name).query(User.age). \
            filter(User.name == 'zs').first()
        if data:
            print('访问的数据库:%s, 具体数据为:%s' % (db_name, data.age))

    db.session().using_bind(None)  # 将指定的数据库进行重置, 否则后续的数据操作都只访问db2
    User.query.all()

    return 'index'


if __name__ == '__main__':
    # 重置所有继承自db.Model的表
    # db.drop_all()
    # db.create_all()

    # # 添加测试数据  需要分别往db1和db2中添加一条数据
    # user1 = User(name='zs', age=20)
    # db.session.add(user1)
    # db.session.commit()

    app.run(debug=True)
